/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.metamodel.util;

import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.regex.Pattern;

import org.apache.metamodel.query.QueryParameter;
import org.apache.metamodel.schema.ColumnType;

/**
 * Helper class for formatting
 */
public final class FormatHelper {
    
    private static Pattern DATE_PATTERN = Pattern.compile("(?:(?:DATE *['(\"])|(?:['(\"]))?([^')\"]*)[')\"]?");
    private static Pattern TIME_PATTERN = Pattern.compile("(?:(?:TIME *['(\"])|(?:['(\"]))?([^')\"]*)[')\"]?");
    private static Pattern TIMESTAMP_PATTERN = Pattern.compile("(?:(?:TIMESTAMP *['(\"])|(?:['(\"]))?([^')\"]*)[')\"]?");

    /**
     * Creates a uniform number format which is similar to that of eg. Java
     * doubles. The format will not include thousand separators and it will use
     * a dot as a decimal separator.
     * 
     * @return
     */
    public static NumberFormat getUiNumberFormat() {
        DecimalFormatSymbols symbols = new DecimalFormatSymbols();
        symbols.setDecimalSeparator('.');
        DecimalFormat format = new DecimalFormat("###.##", symbols);
        format.setGroupingUsed(false);
        format.setMaximumFractionDigits(Integer.MAX_VALUE);
        return format;
    }

    public static NumberFormat getSqlNumberFormat() {
        DecimalFormatSymbols symbols = new DecimalFormatSymbols();
        symbols.setDecimalSeparator('.');
        DecimalFormat format = new DecimalFormat("###.##", symbols);
        format.setGroupingUsed(false);
        format.setMaximumFractionDigits(100);
        return format;
    }

    public static String formatSqlBoolean(ColumnType columnType, boolean b) {
        if (columnType == ColumnType.BIT) {
            if (b) {
                return "1";
            } else {
                return "0";
            }
        } else {
            if (b) {
                return "TRUE";
            } else {
                return "FALSE";
            }
        }
    }

    /**
     * Formats a date according to a specific column type (DATE, TIME or
     * TIMESTAMP)
     * 
     * @param columnType
     *            the column type
     * @param date
     *            the date value
     * @return
     */
    public static String formatSqlTime(ColumnType columnType, Date date) {
        return formatSqlTime(columnType, date, true);
    }

    /**
     * Formats a date according to a specific column type (DATE, TIME or
     * TIMESTAMP)
     * 
     * @param columnType
     *            the column type
     * @param date
     *            the date value
     * @param typeCastDeclaration
     *            whether or not to include a type cast declaration
     * @param beforeDateLiteral
     *            before date literal
     * @param afterDateLiteral
     *            after date literal
     * @return
     */
    public static String formatSqlTime(ColumnType columnType, Date date, boolean typeCastDeclaration,
            String beforeDateLiteral, String afterDateLiteral) {
        if (columnType == null) {
            throw new IllegalArgumentException("Column type cannot be null");
        }
        final DateFormat format;
        final String typePrefix;
        if (columnType.isTimeBased()) {
            if (columnType == ColumnType.DATE) {
                format = DateUtils.createDateFormat("yyyy-MM-dd");
                typePrefix = "DATE";
            } else if (columnType == ColumnType.TIME) {
                format = DateUtils.createDateFormat("HH:mm:ss");
                typePrefix = "TIME";
            } else {
                format = DateUtils.createDateFormat("yyyy-MM-dd HH:mm:ss");
                typePrefix = "TIMESTAMP";
            }

        } else {
            throw new IllegalArgumentException("Cannot format time value of type: " + columnType);
        }

        if (typeCastDeclaration) {
            return typePrefix + " " + beforeDateLiteral + format.format(date) + afterDateLiteral;
        } else {
            return format.format(date);
        }
    }

    /**
     * Formats a date according to a specific column type (DATE, TIME or
     * TIMESTAMP). For backward compatibility.
     * 
     * @param columnType
     * @param date
     * @param typeCastDeclaration
     * @return
     */
    public static String formatSqlTime(ColumnType columnType, Date date, boolean typeCastDeclaration) {
        return formatSqlTime(columnType, date, typeCastDeclaration, "\'", "\'");
    }

    /**
     * Parses a SQL string representation of a time based value
     * 
     * @param columnType
     * @param value
     * @return
     */
    public static Date parseSqlTime(ColumnType columnType, String value) {
        final String[] formats;
        if (columnType.isTimeBased()) {
            if (columnType == ColumnType.DATE) {
                value = DATE_PATTERN.matcher(value).replaceFirst("$1");
                formats = new String[] { "yyyy-MM-dd" };
            } else if (columnType == ColumnType.TIME) {
                value = TIME_PATTERN.matcher(value).replaceFirst("$1");
                formats = new String[] { "HH:mm:ss.SSS", "HH:mm:ss", "HH:mm" };
            } else {
                value = TIMESTAMP_PATTERN.matcher(value).replaceFirst("$1");
                formats = new String[] { "yyyy-MM-dd HH:mm:ss.SSS", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM-dd" };
            }
        } else {
            throw new IllegalArgumentException("Cannot parse time value of type: " + columnType);
        }

        for (String format : formats) {
            try {
                DateFormat dateFormat = DateUtils.createDateFormat(format);
                return dateFormat.parse(value);
            } catch (ParseException e) {
                // proceed to next format
            }
        }

        throw new IllegalArgumentException("String value '" + value + "' not parsable as a " + columnType);
    }

    public static String formatSqlValue(ColumnType columnType, Object value) {
        if (value == null) {
            return "NULL";
        }
        if (value instanceof QueryParameter) {
            return value.toString();
        }
        if (value.getClass().isArray()) {
            value = CollectionUtils.toList(value);
        }
        if (value instanceof Iterable) {
            StringBuilder sb = new StringBuilder();
            sb.append('(');
            for (Object item : (Iterable<?>) value) {
                if (sb.length() > 1) {
                    sb.append(" , ");
                }
                sb.append(formatSqlValue(columnType, item));
            }
            sb.append(')');
            return sb.toString();
        } else if (isNumber(columnType, value)) {
            NumberFormat numberFormat = getSqlNumberFormat();
            Number n = NumberComparator.toNumber(value);
            if (n == null) {
                throw new IllegalStateException("Could not convert " + value + " to number");
            }
            String numberString = numberFormat.format(n);
            return numberString;
        } else if (isBoolean(columnType, value)) {
            Boolean b = BooleanComparator.toBoolean(value);
            if (b == null) {
                throw new IllegalStateException("Could not convert " + value + " to boolean");
            }
            String booleanString = formatSqlBoolean(columnType, b);
            return booleanString;
        } else if (isTimeBased(columnType, value)) {
            Date date = TimeComparator.toDate(value);
            if (date == null) {
                throw new IllegalStateException("Could not convert " + value + " to date");
            }
            String timeString = formatSqlTime(columnType, date);
            return timeString;
        } else if (isLiteral(columnType, value)) {
            return '\'' + value.toString() + '\'';
        } else {
            if (columnType == null) {
                throw new IllegalStateException("Value type not supported: " + value);
            }
            throw new IllegalStateException("Column type not supported: " + columnType);
        }
    }

    private static boolean isTimeBased(ColumnType columnType, Object operand) {
        if (columnType == null) {
            return TimeComparator.isTimeBased(operand);
        }
        return columnType.isTimeBased();
    }

    private static boolean isBoolean(ColumnType columnType, Object operand) {
        if (columnType == null) {
            return operand instanceof Boolean;
        }
        return columnType.isBoolean();
    }

    private static boolean isNumber(ColumnType columnType, Object operand) {
        if (columnType == null) {
            return operand instanceof Number;
        }
        return columnType.isNumber();
    }

    private static boolean isLiteral(ColumnType columnType, Object operand) {
        if (columnType == null) {
            return operand instanceof String;
        }
        return columnType.isLiteral();
    }
}